/**
 *  Copyright (C) 2008-2013  Telosys project org. ( http://www.telosys.org/ )
 *
 *  Licensed under the GNU LESSER GENERAL PUBLIC LICENSE, Version 3.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *          http://www.gnu.org/licenses/lgpl.html
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.telosys.util;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.telosys.util.StrUtil;

/* package */ class BeanUtilSubPart1 {

    private final static String CANNOT_GET_CONSTRUCTOR = "cannot get constructor(String) for class " ;
    
    private final static String CONSTRUCTOR_ERROR      = "constructor(String) error for class " ;
    
    //----------------------------------------------------------------------------------------------
    private static char toChar( String s )
    {
        int length = s.length();
        if ( length == 0 ) // ""
        {
            throw new RuntimeException("Cannot convert a void string to 'char'" );
        }
        if ( length == 1 ) // "A", "B", ...
        {
            return s.charAt(0); // Use the character "as is"
        }
        else // more than one char : Special char, or Octal, or Unicode expected
        {
            if ( s.charAt(0) == '\\' )
            {
                if ( length == 2 ) // Special value expected : "\0", "\n", "\f", ...
                {
                    char c2 = s.charAt(1);
                    switch (c2)
                    {
                    case '0' : return '\0' ;
                    case 'b' : return '\b' ;
                    case 'f' : return '\f' ;
                    case 'n' : return '\n' ;
                    case 'r' : return '\r' ;
                    case 't' : return '\t' ;
                    default : return c2 ; 
                    }
                }
                if ( length == 4 ) // Octal value expected : "\040" , ...
                {
                    return (char) Byte.parseByte( s.substring(1), 8 );
                }
                if ( length == 6 ) // Unicode ( 2 Hexa chars ) expected : "\u00F6", "\u0041", ...
                {
                    return (char) Byte.parseByte( s.substring(2), 16 ); 
                }
            }
        }
        throw new RuntimeException("Cannot convert '" + s + "' to 'char'" );
        
    }
    //----------------------------------------------------------------------------------------------
    private static Object callConstructorWithStringArgument( Class<?> clazz, String sValue )
    {
        //$log.trace("callConstructorWithStringArgument( " + clazz.getName() + ", '" + sValue + "' )");
        Object newInstance = null ;
        
        Class<?>[]  argTypes  = { String.class } ; 
        Object[] argValues = { sValue } ; 

        //--- Get the constructor
        Constructor<?> constructor = null ;
        try
        {
            constructor = clazz.getConstructor( argTypes );
        } catch (SecurityException e)
        {
            throw new RuntimeException( CANNOT_GET_CONSTRUCTOR + clazz.getName() + " : SecurityException ", e);
        } catch (NoSuchMethodException e)
        {
            throw new RuntimeException( CANNOT_GET_CONSTRUCTOR + clazz.getName() + " : NoSuchMethodException ", e);
        }

        //--- Call the constructor
        if ( constructor != null )
        {
            try
            {
                newInstance = constructor.newInstance(argValues);
            } catch (IllegalArgumentException e1)
            {
                throw new RuntimeException(CONSTRUCTOR_ERROR + clazz.getName() + " : IllegalArgumentException ", e1);
            } catch (InstantiationException e1)
            {
                throw new RuntimeException(CONSTRUCTOR_ERROR + clazz.getName() + " : InstantiationException ", e1);
            } catch (IllegalAccessException e1)
            {
                throw new RuntimeException(CONSTRUCTOR_ERROR + clazz.getName() + " : IllegalAccessException ", e1);
            } catch (InvocationTargetException e1)
            {
                Throwable cause = e1.getCause() ;
                if ( cause != null )
                {
                    if ( cause instanceof NumberFormatException )
                    {
                        throw new RuntimeException("Cannot create '" + clazz.getName() + "' with value '" + sValue + "' : NumberFormatException", cause );
                    }
                }
                throw new RuntimeException(CONSTRUCTOR_ERROR + clazz.getName() + " : InvocationTargetException ", e1);
            }
        }
        else
        {
            throw new RuntimeException("cannot get constructor(String) for class " + clazz.getName() + " ");
        }
        return newInstance ;
    }
    
    //----------------------------------------------------------------------------------------------
    /**
     * Converts the given value to the expected Object type <br>
     * 
     * @param sValue
     * @param cType the parameter type expected
     * @return the instance in the expected type, or null if sValue is null or void ( length == 0 ) 
     */
    private static Object convertParam( String sValue, Class<?> cType )
    {
        // $log.trace("convertParam( '" + sValue + "' ," + cType.getName() + ")");
        
        String sTypeName = cType.getName();
        // $log.trace("convertParam : type name = '" + sTypeName + "'");
        
		if ( cType.isPrimitive() ) 
		{
	        //--- Cannot be null
	        // $log.trace("convertParam : primitive type ");
	        if ( sValue == null ) throw new RuntimeException("Cannot set null value for primitive type '" + sTypeName + "'" ) ;
	        
	        //--- For these types the spaces are not significant and can be removed " 12 " --> "12"
	        String sTrimedValue = sValue.trim();
	        if ( sTrimedValue.length() == 0 ) throw new RuntimeException("Cannot set void value for primitive type '" + sTypeName + "'" ) ;
	        
	        //--- The 8 Java primitive types :
		    try
            {
                if ( "int".equals(sTypeName) ) return new Integer(sTrimedValue);
                if ( "long".equals(sTypeName) ) return new Long(sTrimedValue);
                if ( "short".equals(sTypeName) ) return new Short(sTrimedValue);
                if ( "byte".equals(sTypeName) ) return new Byte(sTrimedValue);
                
                if ( "double".equals(sTypeName) ) return new Double(sTrimedValue);
                if ( "float".equals(sTypeName) ) return new Float(sTrimedValue);
            } catch (NumberFormatException e)
            {
                throw new RuntimeException("Cannot convert '" + sTrimedValue + "' to '" + sTypeName + "' : NumberFormatException", e );
            }

		    if ( "boolean".equals(sTypeName) ) 
		    {
		        return Boolean.valueOf(sTrimedValue); // true if "true".equalsIgnoreCase(sValue)
		    }

		    if ( "char".equals(sTypeName) ) 
		    {
		        // Different cases : "\u00F6", "\u0041\u0042\u0043", "\n", ...
		        return new Character( toChar(sTrimedValue) ); // trimed value => for blank use "\u0020" or "\040" 
		    }

		}
		else
		{
	        //--- Can be null
	        if ( sValue == null ) return null ;
	        if ( sValue.length() == 0 ) return null ;
	        
	        
	        if ( "java.lang.String".equals ( sTypeName ) )
	        {
	            // No trim : keep the string as is
	            return sValue ;
	        }
	        if ( "java.lang.Boolean".equals ( sTypeName ) )
	        {
		        //--- For this type the spaces are not significant and can be removed " true " --> "true"
		        String sTrimedValue = sValue.trim();
		        if ( sTrimedValue.length() == 0 ) throw new RuntimeException("Cannot set void value for Boolean type" ) ;
	            return Boolean.valueOf(sTrimedValue); // true if "true".equalsIgnoreCase(sValue)
	        }
	        
	        
	        if ( "java.util.Date".equals ( sTypeName ) )
	        {
	            //--- Spaces are not significant, but trim is done by getDate
	            return StrUtil.getDate(sValue); // "YYYY-MM-DD", "HH:MM:SS" or "YYYY-MM-DD HH:MM:SS"
	        }
	        
	        if ( "java.sql.Date".equals ( sTypeName ) )
	        {
	            //--- Spaces are not significant, but trim is done by getDate
	            java.util.Date date = StrUtil.getDate(sValue); // "YYYY-MM-DD", "HH:MM:SS" or "YYYY-MM-DD HH:MM:SS"
	            return new java.sql.Date( date.getTime() ); // Keep only the "YYYY-MM-DD" part, "1970-01-01" if no date
	        }
	        if ( "java.sql.Time".equals ( sTypeName ) )
	        {
	            //--- Spaces are not significant, but trim is done by getDate
	            java.util.Date date = StrUtil.getDate(sValue); // "YYYY-MM-DD", "HH:MM:SS" or "YYYY-MM-DD HH:MM:SS"
	            return new java.sql.Time( date.getTime() ); // Keep only the "HH:MM:SS" part, "00:00:00" if no time
	        }
	        if ( "java.sql.Timestamp".equals ( sTypeName ) )
	        {
	            //--- Spaces are not significant, but trim is done by getDate
	            java.util.Date date = StrUtil.getDate(sValue); // "YYYY-MM-DD", "HH:MM:SS" or "YYYY-MM-DD HH:MM:SS"
	            return new java.sql.Timestamp( date.getTime() );
	        }

	        //--- Number wrappers : BigDecimal, BigInteger, Byte, Double, Float, Integer, Long, Short
	        Class<?> superClass = cType.getSuperclass();
	        String sSuperClassName = superClass.getName();
	        if ( "java.lang.Number".equals(sSuperClassName) )
	        {
		        //--- For these types the spaces are not significant and can be removed " 12 " --> "12"
		        String sTrimedValue = sValue.trim();
		        if ( sTrimedValue.length() == 0 ) throw new RuntimeException("Cannot set void value for Number type '" + sTypeName + "'" ) ;
	            
	            //--- All the classes derived from Number have a constructor(String)
	            return callConstructorWithStringArgument( cType, sTrimedValue );
	        }		    

	        if ( "java.lang.Character".equals ( sTypeName ) )
	        {
		        //--- For this type the spaces are not significant and can be removed " \u00F6 " --> "\u00F6"
		        String sTrimedValue = sValue.trim();
		        if ( sTrimedValue.length() == 0 ) throw new RuntimeException("Cannot set void value for Character type" ) ;
		        // Different cases : "\u00F6", "\101", "\n", ...
		        return new Character( toChar(sTrimedValue) ) ; 
	        }

	        if ( "java.lang.StringBuffer".equals ( sTypeName ) )
	        {
	            // No trim : keep the string as is
	            return new StringBuffer(sValue) ;
	        }
		}
		throw new RuntimeException("Type " + sTypeName + " not supported" );
    }
    //----------------------------------------------------------------------------------------------
    private static void callSetterMethod(Method method, Object oBean, Object oValue)
    {
        Object[] params = new Object[1];
        params[0] = oValue ;
        try
        {
            method.invoke(oBean, params);
        } catch (IllegalArgumentException e)
        {
            throw new RuntimeException("callSetterMethod : IllegalArgumentException ", e);
            //return null;
        } catch (IllegalAccessException e)
        {
            throw new RuntimeException("callSetterMethod : IllegalAccessException ", e);
            //return null;
        } catch (InvocationTargetException e)
        {
            throw new RuntimeException("callSetterMethod : InvocationTargetException ", e);
            //return null;
        }
    }
    //----------------------------------------------------------------------------------------------
    private static Method findMethodByName(Class<?> beanClass, String sMethodName)
    {
        Method[] methods = null;
        try
        {
            methods = beanClass.getMethods();
            
        } catch (SecurityException e)
        {
            throw new RuntimeException("findMethodByName : security violation, class = " + beanClass.getName(), e);
        }
        
        Method m = null;
        if ( methods != null )
        {
            for ( int i = 0 ; i < methods.length ; i++ )
            {
                m = methods[i] ;
                if ( sMethodName.equals( m.getName() ) )
                {
                    return m ; // Found
                }
            }
        }
        return null ; // Not found
    }
    
	/* package */ static void setFieldValue(Object oBean, String sFieldName, String sFieldValue) 
	{
		if (oBean == null) {
			throw new IllegalArgumentException("setFieldValue : bean instance is null ");
		}
		if (sFieldName == null) {
			throw new IllegalArgumentException("setFieldValue : field name is null ");
		}

		String sSetterName = "set" + sFieldName.substring(0, 1).toUpperCase() + sFieldName.substring(1);

		Method method = findMethodByName(oBean.getClass(), sSetterName);
		if ( method != null )
		{
			Class<?>[] paramTypes = method.getParameterTypes();
			if ( paramTypes.length == 1 )
			{
			    Object oValue = convertParam( sFieldValue, paramTypes[0] );
			    callSetterMethod(method, oBean, oValue);
			    return ;
			}
		}
	    throw new RuntimeException("setFieldValue : no method '" + sSetterName + "(arg)' in class = " + oBean.getClass().getName() );
	}
	
}
